#include "FindWaterAreas.h"

#include "Model3D.h"
#include "HeightMap.h"
#include "HeightMapCoordinates.h"
#include "SharedConstants.h"
#include "GenerateSurface.h"
#include "Water.h"

#include <queue>

namespace
  {
  using namespace HeightMapUtils;


  /**
  *  Finds points below certain level starting from i_point and proceeding to its neighbors
  *  i_point is assumed to be already below this level
  */
  void FindPointsInArea(const HeightMap &i_heightmap, THMapPoint &i_point, std::set<THMapPoint> &io_area,
    double i_level)
    {
    enum ECellVisited {CV_VISITED, CV_NOT_VISITED};

    std::vector<std::vector<ECellVisited>> visited_cells(i_heightmap.GetSizeX(), std::vector<ECellVisited>(i_heightmap.GetSizeY(), CV_NOT_VISITED));
    std::queue<THMapPoint> point_queue;

    point_queue.push(i_point);

    while (point_queue.empty() == false)
      {
      THMapPoint current_point = point_queue.front();
      point_queue.pop();

      io_area.insert(current_point);

      size_t x = current_point.first;
      size_t y = current_point.second;

      visited_cells[x][y] = CV_VISITED;

      if(x > 0 && i_heightmap.Get(x - 1, y) <= i_level && visited_cells[x - 1][y] == CV_NOT_VISITED)
        {
        point_queue.push(THMapPoint(x-1, y));
        visited_cells[x - 1][y] = CV_VISITED;
        }

      if(y > 0 && i_heightmap.Get(x, y - 1) <= i_level && visited_cells[x][y - 1] == CV_NOT_VISITED)
        {
        point_queue.push(THMapPoint(x, y - 1));
        visited_cells[x][y - 1] = CV_VISITED;
        }
      
      if(x < i_heightmap.GetSizeX() - 1 && i_heightmap.Get(x + 1, y) <= i_level && visited_cells[x + 1][y] == CV_NOT_VISITED)
        {
        point_queue.push(THMapPoint(x + 1, y));
        visited_cells[x + 1][y] = CV_VISITED;
        }

      if(y < i_heightmap.GetSizeY() - 1 && i_heightmap.Get(x, y + 1) <= i_level && visited_cells[x][y + 1] == CV_NOT_VISITED) 
        {
        point_queue.push(THMapPoint(x, y + 1));
        visited_cells[x][y + 1] = CV_VISITED;
        }
      }

    }

  /**
  *  Finds water area below certain level starting from i_point
  *  i_point is assumed to be below that level
  */
  THMapArea FindWaterAreaFromPoint(const HeightMap &i_heighmap, THMapPoint i_point, double i_level)
    {
    THMapArea area;
    area.level = i_level;
    std::set<THMapPoint> points;
    FindPointsInArea(i_heighmap, i_point, points, i_level);
    area.points = points;

    return area;
    }

  /**
  * Finds local minimums in i_heightmap
  */
  std::set<THMapPoint> FindHeightMapMins(const HeightMap &i_heightmap)
    {
    size_t size_x = i_heightmap.GetSizeX();
    size_t size_y = i_heightmap.GetSizeY();

    std::set<THMapPoint> result;

    bool min_flag = true;

    for(size_t i = 0; i < size_x; i++)
      {
      for(size_t j = 0; j < size_y; j++)
        {
        double val = i_heightmap.Get(i, j);

        if(i > 0)
          {
          min_flag &= val <= i_heightmap.Get(i - 1, j);
          }

        if(i < size_x - 1)
          {
          min_flag &= val <= i_heightmap.Get(i + 1, j);
          }

        if(j > 0)
          {
          min_flag &= val <= i_heightmap.Get(i, j - 1);
          }

        if(j < size_y - 1)
          {
          min_flag &= val <= i_heightmap.Get(i, j + 1);
          }

        if(min_flag)
          {
          result.insert(THMapPoint(i, j)); 
          }

        min_flag = true;
        }
      }

    return result;
    }
  }


namespace HeightMapUtils
  {
  std::vector<THMapArea> FindPossibleWaterAreas(const HeightMap &i_heightmap, double level)
    {
    std::vector<THMapArea> result;
    std::set<THMapPoint> minimums = FindHeightMapMins(i_heightmap);

    std::set<THMapPoint> used;
    for(auto it = minimums.begin(); it != minimums.end(); it++)
      {
      if(used.count(*it) != 0 || i_heightmap.Get(it->first, it->second) > level)
        continue;

      THMapArea area = FindWaterAreaFromPoint(i_heightmap, *it, level);
      used.insert(area.points.begin(), area.points.end());
      result.push_back(area);
      }
    return result;
    }

  std::vector<std::shared_ptr<Water>> GetWaterModels(const HeightMap &i_heightmap, double i_level)
    {
    std::vector<std::shared_ptr<Water>> result;
    auto areas = FindPossibleWaterAreas(i_heightmap, i_level);
    for(THMapArea area : areas)
      {
      std::shared_ptr<Model3D> pmodel = GenerateWaterModel(area, i_heightmap.GetSizeX(), i_heightmap.GetSizeY());
      std::shared_ptr<Water> pwater(new Water(GroundConstants::ORIGIN_VECTOR, area, pmodel));
      result.push_back(pwater);
      }
    return result;
    }

  std::shared_ptr<Model3D> GenerateWaterModel(const THMapArea& i_area, size_t i_max_x, size_t i_max_z)
    {
    std::shared_ptr<Model3D> p_model(new Model3D);

    for(THMapPoint point: i_area.points)
      {
      size_t x = point.first;
      size_t z = point.second;

      Point3DID* point = &p_model->AddPoint(ConvertHMapPointToTVector(x, z, GroundConstants::CELL_SIZE * i_area.level));


      Point3DID* point1 = nullptr;
      Point3DID* point2 = nullptr; 
      Point3DID* point3 = nullptr; 
      Point3DID* point4 = nullptr; 
      Point3DID* point5 = nullptr;
      Point3DID* point6 = nullptr;

      if(x > 0)
        {
        point1 = &p_model->AddPoint(ConvertHMapPointToTVector(x - 1, z, GroundConstants::CELL_SIZE * i_area.level));
        }
      if(x > 0 && z < i_max_z - 1)
        {
        point2 = &p_model->AddPoint(ConvertHMapPointToTVector(x - 1, z + 1, GroundConstants::CELL_SIZE * i_area.level));
        }
      if(z < i_max_z - 1)
        {
        point3 = &p_model->AddPoint(ConvertHMapPointToTVector(x, z + 1, GroundConstants::CELL_SIZE * i_area.level));
        }
      if(x < i_max_x - 1)
        {
        point4 = &p_model->AddPoint(ConvertHMapPointToTVector(x + 1, z, GroundConstants::CELL_SIZE * i_area.level));
        }
      if(x < i_max_x - 1 && z > 0)
        {
        point5 = &p_model->AddPoint(ConvertHMapPointToTVector(x + 1, z - 1, GroundConstants::CELL_SIZE * i_area.level));
        }
      if(z > 0)
        {
        point6 = &p_model->AddPoint(ConvertHMapPointToTVector(x, z - 1, GroundConstants::CELL_SIZE * i_area.level));
        }

      if(x > 0 && z < i_max_z - 1)
        {
        p_model->AddTriangle(*point, *point1, *point2);
        p_model->AddTriangle(*point, *point2, *point3);
        }
      if(x < i_max_x - 1 && z < i_max_z - 1)
        {
        p_model->AddTriangle(*point, *point3, *point4);
        }
      if(x > 0 && z > 0)
        {
        p_model->AddTriangle(*point, *point6, *point1);
        }
      if(x < i_max_x - 1 && z > 0)
        {
        p_model->AddTriangle(*point, *point4, *point5);
        p_model->AddTriangle(*point, *point5, *point6);
        }
      }
    return p_model;
    }

  }

